﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
using NDBGen.Plugins;
using NDBGen.Plugins.DataAccess;

namespace NDBGen
{
    [DataAccessProjectFileManager("VisualStudio")]
    public class VisualStudioProjectFileManager : IProjectFileManager
    {
        private Byte[] originalStateHash;
        private XDocument innerDoc;

        public VisualStudioProjectFileManager()
        {
            this.AddQueue = new List<XElement[]>();
        }

        public void Load(String path)
        {
            if (this.FilePath != path)
            {
                if (!this.AddQueue.Any())
                {
                    this.FilePath = path;
                    innerDoc = XDocument.Load(path);

                    originalStateHash = this.GetFileStateHash();

                    this.RemoveGeneratedFileReferences();
                }
                else throw new Exception("A project file has been loaded and may have pending changes. It must be saved before loading a new file.");
            }
        }

        private Byte[] GetFileStateHash()
        {
            var md5 = MD5.Create();
            using(var stream = this.GetFileAsStream())
            {
                var bytes = new Byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);

                return md5.ComputeHash(bytes);
            }
        }

        private Stream GetFileAsStream()
        {
            var stream = new MemoryStream();
            var writer = new StreamWriter(stream);
            innerDoc.Save(writer);
            stream.Flush();
            stream.Position = 0;
            return stream;
        }

        public String FilePath { get; set; }

        public void Save()
        {
            foreach (var queueItem in this.AddQueue)
            {
                queueItem[0].Add(queueItem[1]);
            }

            var currentStateHash = this.GetFileStateHash();

            if (!currentStateHash.SequenceEqual(originalStateHash))
            {
                innerDoc.Save(this.FilePath);
            }

            originalStateHash = currentStateHash;
            this.AddQueue.Clear();
        }

        public XName Name(String localName)
        {
            return XName.Get(localName, innerDoc.Root.GetDefaultNamespace().NamespaceName);
        }

        private void RemoveGeneratedFileReferences()
        {
            var generatedElements = (
                from itemGroup in innerDoc.Root.Descendants(this.Name("ItemGroup"))
                from entry in itemGroup.Descendants()
                where entry.Attribute("Include") != null && entry.Attribute("Include").Value.Contains(".g.")
                select entry).ToList();

            var removed = 0;
            for (var index = 0; index < generatedElements.Count; index++)
            {
                var compile = generatedElements[index];
                compile.Remove();
                removed++;
            }
        }

        public List<XElement[]> AddQueue { get; private set; }

        private String GetElementNameByExtension(String extension)
        {
            switch (extension)
            {
                case ".cs":
                case ".vb": return "Compile";

                default: return "None";
            }
        }
        
        public void AddFileReference(String relativePath, String dependentFile)
        {
            var elementName = GetElementNameByExtension(new FileInfo(relativePath).Extension);
            var group = (
                from itemGroup in innerDoc.Descendants(this.Name("ItemGroup"))
                where itemGroup.Descendants(this.Name(elementName)).Any()
                select itemGroup).FirstOrDefault() ?? innerDoc.Descendants(this.Name("ItemGroup")).SingleOrDefault(e => !e.HasElements);

            if (group == null)
            {
                group = new XElement(this.Name("ItemGroup"));
                innerDoc.Root.Add(group);
            }

            var element = new XElement(this.Name(elementName), new XAttribute("Include", relativePath));
            if (!String.IsNullOrEmpty(dependentFile))
            {
                element.Add(new XElement(this.Name("DependentUpon"), dependentFile));
            }

            var existingPlusQueue = this.AddQueue.SelectMany(q => q).Union(
                from itemGroup in innerDoc.Descendants("ItemGroup")
                from entry in itemGroup.Descendants()
                where entry.Attribute("Include") != null && entry.Attribute("Include").Value.Contains(".g.")
                select entry);

            if (!existingPlusQueue.Any(i => i.Attribute("Include") != null && i.Attribute("Include").Value == relativePath))
            {
                this.AddQueue.Add(new XElement[] { group, element });
            }
        }

        public void AddFileReference(String relativePath)
        {
            this.AddFileReference(relativePath, null);
        }

        public GeneratedMergeType GetMergeType(String pathWithNoExtension, out String dependentFile)
        {
            var hardCoded = (
                from compile in innerDoc.Descendants(this.Name("Compile"))
                let include = compile.Attribute("Include").Value
                where include.StartsWith(pathWithNoExtension + ".")
                orderby include.Split('.').Length
                select include).FirstOrDefault();

            if (hardCoded == null)
            {
                dependentFile = null;
                return GeneratedMergeType.GeneratedOnly;
            }
            else
            {
                dependentFile = hardCoded.Split('\\').Last();
                var hardCodedFile = new FileInfo(hardCoded);
                if (hardCodedFile.Name.Replace(hardCodedFile.Extension, String.Empty).EndsWith(".s")) return GeneratedMergeType.DoNotGenerate;
                else return GeneratedMergeType.GeneratePartial;
            }
        }
    }
}